{% extends "base.html" %} {% block title %}Pup ID Β· {{ app_name }}{% endblock %} {% block body %} {% macro arrive_form(label) %}
With pup.json: the passphrase it was sealed under. With a sheet: a new one (at least {{ min_pass }} characters), twice, to seal it on this box.
{% endmacro %}

πŸͺͺ Pup ID a key that is you, not this box πŸ“–

{% if ident.state == 'none' %}

Make your Pup ID

A Pup ID is a key that is you: it signs the papers for your badges, it is who a Handler collars and who a pack knows, and it moves with you to a new box.

πŸ”΄ {{ make_warning }}

{% if hosted %}

⚠️ {{ hosted_warning }}

{% endif %}
You are making this key as β€” your own declaration, signed into every paper; nobody checks it, and it can never change {% for k in kinds %}{% endfor %}

🧳 Arrive from another box

Your pup already lives somewhere? Bring it here with your pup.json (your key sealed under your passphrase, your papers, your travelling kit and photos) β€” or with just your recovery sheet, which brings the key alone. Only a box with no Pup ID can arrive.

{{ arrive_form('Arrive') }}
{% else %} {% if banner %}

{{ banner }}

{% endif %}

{{ pup_profile.name or 'Your pup' }}

{{ headline }}

{% if soc and soc.paused %}

πŸ›‘ Paused β€” everything you granted is suspended. Resume on the Consent page (only there, on the web).

{% endif %} {% if soc and soc.packs %}

Packs: {% for p in soc.packs %}{{ p.name or p.uid }}{{ ', ' if not loop.last }}{% endfor %}

{% endif %}
you signed that you are
{{ kind_label.get(ident.kind, ident.kind) }}
fingerprint
{{ ident.fingerprint }}
did
{{ ident.did }}
key
{{ ident.state }}{% if ident.sealed %} Β· sealed copy{% endif %}{% if ident.seal_at_rest %} Β· sealed at rest{% endif %}

Read the fingerprint aloud to compare it β€” it is what a Handler checks against their screen.

{% for w in ident.perms %}

⚠ {{ w }}

{% endfor %}

πŸ“œ Papers ({{ n_papers }}) 🧳 What travels ({{ n_carry }} photos) ⬇ pup.json

{% if ident.state == 'ready' or ident.moving_at %}{% if left_behind %}

⚠ pup.json carries no pack β€” this box is still in {{ left_behind|length }} pack{{ 's' if left_behind|length != 1 }}. Read this before you move by pup.json.

{% endif %} {% else %}{% with moving=ident.moving_at, retired=(ident.state == 'moved') %}{% include "_pupjson_packs.html" %}{% endwith %}{% endif %} {% if n_unpapered and ident.state == 'ready' and not ident.moving_at %}

{{ n_unpapered }} badge(s) get their paper on the next tick.

{% endif %}
{% if ident.state in ('ready', 'locked', 'mismatch', 'missing') %}

Profile

{% if pup_profile.name_given_by %}

Your name was given by {{ pup_profile.name_given_by.name }} with their collar.

{% endif %}
{% endif %}

πŸ” Your key

⚠️ {{ backup_warning }}

{% if hosted %}

⚠️ {{ hosted_warning }}

{% endif %} {% if ident.state == 'locked' %}
{% elif ident.state == 'missing' %}

This box knows your Pup ID but has no key for it. Bring the key home from your pup.json or your recovery sheet β€” only this pup's own key comes back, with any papers this box lacks. The game on this box β€” its kit, its photos, its profile β€” stays exactly as it is.

{{ arrive_form('Bring my key home') }} {% elif ident.state == 'mismatch' %}

The key file here is not the Pup ID this box says it is. Nothing is signed. Put the right identity/pup.key back β€” or restore your bundle onto a fresh box (a restore never goes onto a box that holds a key file).

{% endif %} {% if ident.state == 'ready' %}

{{ 'Change your passphrase' if ident.sealed else 'Seal it with a passphrase' }}

A passphrase seals a copy of your key (at least {{ min_pass }} characters). The sealed copy is what pup.json and the backup zip carry β€” never the plain key.

{% if ident.sealed %}{% endif %}
{% if ident.sealed %}

⚠️ Changing the passphrase re-seals the copy on this box only. A pup.json or backup zip you already made still opens with the old passphrase, and a recovery sheet needs none. If one of those leaked, your key leaked β€” there is no key rotation yet.

{% endif %} {% endif %} {% if ident.state in ('ready', 'locked') %}

Seal at rest

{% if ident.seal_at_rest %}On: only the sealed file is on disk, and the key is held in memory until this app restarts β€” then it waits for your passphrase.{% else %}Off: the plain key file is on this box's disk (0600).{% endif %}

{% if ident.sealed or ident.seal_at_rest %}
{% else %}

Seal it with a passphrase first (above) β€” seal-at-rest deletes the plain file, so the sealed copy has to be one you can open.

{% endif %} {% if ident.sealed %}

Recovery sheet

Your key written out by hand β€” for the day this box and your pup.json are both gone. Anyone holding it holds your pup. It needs your passphrase to print.

{% endif %} {% endif %}

🧳 Moving to a new box

{% if ident.state == 'moved' %} {% if ident.retired and ident.retired.kept_key %}

πŸ”΄ Your key is on another box you did not move to{% if ident.retired.box_fp %} ({{ ident.retired.box_fp }}){% endif %}. Your key was copied. This box signs nothing.

{% else %}

This box is retired: {{ 'another box holds this pup' if ident.retired and ident.retired.reason == 'rehomed' else 'you moved your pup away' }}{% if ident.retired and ident.retired.box_fp %} (fingerprint {{ ident.retired.box_fp }}){% endif %}{% if ident.retired and ident.retired.at %}, since {{ ident.retired.at|local('%Y-%m-%d') }}{% endif %}. The sealed copy of the key stays here.

{% endif %}

Bring my pup back to this box

{% if ident.sealed %}{% endif %}
{% elif ident.moving_at %}

This pup is packing to move: packs, your AI and papers are stopped (since {{ ident.moving_at|local('%Y-%m-%d %H:%M') }}). Your own game β€” the lock, the emergency key, everything β€” keeps working.

  1. Download the whole game and restore it on the new box (python -m latch.restore), or download pup.json and arrive there. pup.json carries no pack: the game's bundle keeps your packs, but by pup.json each den still holds your membership through THIS box and refuses a new join until it is gone β€” so leave your packs before you prepare (or ask their operators to remove you), then join again with fresh tags. {% with moving=True %}{% include "_pupjson_packs.html" %}{% endwith %}
  2. Once your pup is alive on the new box, retire this one: its plain key is zero-overwritten and deleted; the sealed copy stays.

Retiring asks for your passphrase: it proves the sealed copy the new box relies on actually opens before this box deletes its plain key.

{% elif ident.state == 'ready' %}

Moving is explicit. Preparing stops everything that talks to anyone else β€” packs, your AI, papers β€” so nothing is signed twice from two boxes. Cancel any time.

{% with moving=False %}{% include "_pupjson_packs.html" %}{% endwith %}
{% else %}

Unlock your key first.

{% endif %} {% if box_fp %}

This box's own fingerprint: {{ box_fp }}

{% endif %}
{% endif %} {% endblock %}